【QT】 Qt多线程的“那些事” 您所在的位置:网站首页 qt 多线程进度条 【QT】 Qt多线程的“那些事”

【QT】 Qt多线程的“那些事”

#【QT】 Qt多线程的“那些事”| 来源: 网络整理| 查看: 265

作者:李春港 出处:https://www.cnblogs.com/lcgbk/p/13967448.html

目录一、前言二、QThread源码浅析2.1 QThread类的定义源码2.2 QThread::start()源码2.3 QThreadPrivate::start()源码2.4 QThread::run()源码2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码2.6 章节小结三、四种Qt多线程的实现方法3.1 子类化QThread3.1.1 步骤3.1.2 不使用事件循环实例3.1.3 使用事件循环实例3.1.4 子类化QThread线程的信号与槽3.1.5 如何正确退出线程并释放资源3.1.6 小结3.2 子类化QObject+moveToThread3.2.1 步骤3.2.2 实例3.2.3 如何正确使用线程(信号槽)和创建线程资源3.2.4 如何正确退出线程并释放资源3.2.5 小结3.3 继承QRunnable+QThreadPool3.3.1 步骤3.3.2 实例3.3.3 启动线程的方式3.3.4 如何与外界通信3.3.5 小结3.4 QtConcurrent::run()+QThreadPool四、跨线程的信号槽五、总结

一、前言

在我们开发Qt程序时,会经常用到多线程和信号槽的机制,将耗时的事务放到单独的线程,将其与GUI线程独立开,然后通过信号槽的机制来进行数据通信,避免GUI界面假死的情况。例如:使用QT实现文件的传送,并且GUI界面需要实时显示发送的进度,这时就需要将耗时的文件数据操作放到独立的线程中,然后把已传送的进度数据通过信号发送到GUI线程,GUI主线程接收到信号后通过槽函数来更新UI,这样界面就不会出现假死的情况了。 多线程和信号槽机制都是QT的关键技术之一。理解清楚这两个技术点的关系,会让你在开发过程中少走些弯路,少踩一些坑。本文章会介绍多种Qt多线程的实现方法,但是主要还是介绍有关于 信号槽机制的多线程 实现方法。在学习QT多线程的"那些事"前,我们不妨先思考下以下的一些问题,然后再带着问题继续往下看,这样可能会有更好的理解: 【1】如何正确使用QT的多线程? 【2】线程start后,哪里才是线程正在启动的地方? 【3】如何正确结束子线程以及资源释放? 【4】重复调用QThread::start、QThread::quit()或QThread::exit()、QThread::terminate函数会有什么影响? 【5】调用QThread::quit()或QThread::exit()、QThread::terminate函数会不会立刻停止线程? 【6】多线程之间是怎么进行通信的? 【7】如何在子线程中启动信号与槽的机制? 【8】QT中多线程之间的信号和槽是如何发送或执行的? 【9】如何正确使用信号与槽机制?

接下来我会通过我以前踩过的坑和开发经验,并且通过一些实例来总结一下QT多线程和QT信号槽机制的知识点。

这个是本文章实例的源码地址:https://gitee.com/CogenCG/QThreadExample.git

二、QThread源码浅析

本章会挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的。其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进行浅析。

2.1 QThread类的定义源码

Qt4.0.1版本源码:

#ifndef QT_NO_THREAD class Q_CORE_EXPORT QThread : public QObject { public: ...//省略 explicit QThread(QObject *parent = 0); ~QThread(); ...//省略 void exit(int retcode = 0); public slots: void start(QThread::Priority = InheritPriority); //启动线程函数 void terminate(); //强制退出线程函数 void quit(); //线程退出函数 ...//省略 signals: void started(); //线程启动信号 void finished(); //线程结束信号 ...//省略 protected: virtual void run() = 0; int exec(); ...//省略 }; #else // QT_NO_THREAD

Qt5.6.2版本源码:

#ifndef QT_NO_THREAD class Q_CORE_EXPORT QThread : public QObject { Q_OBJECT public: ...//省略 explicit QThread(QObject *parent = Q_NULLPTR); ~QThread(); ...//省略 void exit(int retcode = 0); //线程退出函数 ...//省略 public Q_SLOTS: void start(Priority = InheritPriority); //启动线程函数 void terminate(); //强制退出线程函数 void quit(); //线程退出函数 ...//省略 Q_SIGNALS: void started(QPrivateSignal); //线程启动信号 void finished(QPrivateSignal); //线程结束信号 protected: virtual void run(); int exec(); ...//省略 }; #else // QT_NO_THREAD

从以上两个版本的代码可以看出,这些函数在声明上基本没什么差异,但是仔细看,两个版本的 run() 函数声明的是不是不一样?

Qt4.0.1版本run() 函数是纯虚函数,即此类为抽象类不可以创建实例,只可以创建指向该类的指针,也就是说如果你需要使用QThread来实现多线程,就必须实现QThread的派生类并且实现 run() 函数; Qt5.6.2版本的run() 函数是虚函数,继承QThread类时,可以重新实现 run() 函数,也可以不实现。

注:我查看了多个Qt版本的源码,发现出现以上差异的版本是从Qt4.4开始的。从Qt4.4版本开始,QThread类就不再是抽象类了。

2.2 QThread::start()源码

再来看看QThread::start()源码,Qt4.0.1版本和Qt5.6.2版本此部分的源码大同小异,所以以Qt5.6.2版本的源码为主,如下:

void QThread::start(Priority priority) { Q_D(QThread); QMutexLocker locker(&d->mutex); if (d->isInFinish) { locker.unlock(); wait(); locker.relock(); } if (d->running) return; ... ... // 此部分是d指针配置 #ifndef Q_OS_WINRT ... ... // 此部分为注释 d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start, this, CREATE_SUSPENDED, &(d->id)); #else // !Q_OS_WINRT d->handle = (Qt::HANDLE) CreateThread(NULL, d->stackSize, (LPTHREAD_START_ROUTINE)QThreadPrivate::start, this, CREATE_SUSPENDED, reinterpret_cast(&d->id)); #endif // Q_OS_WINRT if (!d->handle) { qErrnoWarning(errno, "QThread::start: Failed to create thread"); d->running = false; d->finished = true; return; } int prio; d->priority = priority; switch (d->priority) { ... ... // 此部分为线程优先级配置 case InheritPriority: default: prio = GetThreadPriority(GetCurrentThread()); break; } if (!SetThreadPriority(d->handle, prio)) { qErrnoWarning("QThread::start: Failed to set thread priority"); } if (ResumeThread(d->handle) == (DWORD) -1) { qErrnoWarning("QThread::start: Failed to resume new thread"); } }

挑出里面的重点来说明:

(1)Q_D()宏定义

在看源码的时候,当时比较好奇start函数的第一条语句 Q_D()宏定义 是什么意思,所以就看了下源码,在此也顺便讲讲,Q_D() 源码是一个宏定义,如下:

#define Q_D(Class) Class##Private * const d = d_func()

此处利用了预处理宏里的 ## 操作符:连接前后两个符号,变成一个新的符号。将Q_D(QThread)展开后,变成:QThreadPrivate * const d = d_func()。

(2)_beginthreadex()函数 上面d->handle = (Qt::HANDLE) _beginthreadex ( NULL, d->stackSize, QThreadPrivate::start, this, CREATE_SUSPENDED, &( d->id ) ) 语句中的函数是创建线程的函数,其原型以及各参数的说明如下:

unsigned long _beginthreadex( void *security, // 安全属性,NULL为默认安全属性 unsigned stack_size, // 指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0 unsigned ( __stdcall *start_address )( void * ), // 指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址) void *arglist, // 传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针 //如果传入this,这个this表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身 unsigned initflag, // 线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂) unsigned *thrdaddr // 用于记录线程ID的地址 ); 2.3 QThreadPrivate::start()源码

从QThread::start()源码可以知道,QThreadPrivate::start是重点,其实际就是调用了QThreadPrivate::start(this),这个 this 表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身。因为两个Qt版本此部分的源码大同小异,所以本部分主要是以5.6.2版本的源码为主,其源码以及说明如下:

// 参数arg就是上面所说的this unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg) { QThread *thr = reinterpret_cast(arg); QThreadData *data = QThreadData::get2(thr); // 创建线程局部存储变量,存放线程id qt_create_tls(); TlsSetValue(qt_current_thread_data_tls_index, data); data->threadId = reinterpret_cast(quintptr(GetCurrentThreadId())); QThread::setTerminationEnabled(false); { QMutexLocker locker(&thr->d_func()->mutex); data->quitNow = thr->d_func()->exited; } if (data->eventDispatcher.load()) // custom event dispatcher set? data->eventDispatcher.load()->startingUp(); else createEventDispatcher(data); ...//省略 emit thr->started(QThread::QPrivateSignal()); // 发射线程启动信号 QThread::setTerminationEnabled(true); thr->run(); // 调用QThread::run()函数 -- 线程函数 finish(arg); //结束线程 return 0; }

由上述源码可以看出,实际上 run() 函数是在这里调用的,并且发出了 started() 启动信号,等到 run() 函数执行完毕,最后是调用了 QThreadPrivate::finish 函数结束线程,并且在finish内会发出 QThread::finished() 线程已结束的信号。

2.4 QThread::run()源码

再看看QThread::run()函数的源码。在上面 《2.1 QThread类的定义源码》的小节,我们可以看到两个Qt版本声明此方法的方式不一样,Qt-4.0版本将此定义为了纯虚函数,而Qt-5.6版本将此定义为了虚函数,那我们就看看Qt-5.6版本中,QThread::run()是如何定义的,如下:

void QThread::run() { (void) exec(); }

每一个 Qt 应用程序至少有一个 事件循环 ,就是调用了 QCoreApplication::exec() 的那个事件循环。不过,QThread也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用main()函数的那个线程,并且由 QCoreApplication::exec() 创建开启的那个事件循环成为 主事件循环 ,或者直接叫 主循环 。注意,QCoreApplication::exec()只能在调用main()函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。QThread的局部事件循环则可以通过在 QThread::run() 中调用 QThread::exec() 开启。

我们通过以上源码可以看到,它的定义很简单,就是调用了一个函数:QThread::exec() 开启线程中的 事件循环 ,我们也可以通过继承QThread,重写run()函数的方式,让其实现相对复杂的逻辑代码。如果你的线程需要将某些槽函数在本线程完成的话,就必须开启事件循环,否则在线程内无法响应各种信号并作出相应的行为。

小结: 比Qt-4.4版本更早的版本中,我们使用QThread启动线程时,就必须要实现继承于QThread的派生类,并且一定要重写run函数,若需要使用事件循环,就需要在run函数中添加exec()。到了Qt4.4版本之后(包括Qt4.4版本),QThread就不是抽象类了,不派生也可以实例化,在不重写QThread::run()方法,start启动线程是默认启动事件循环的。

注:当程序跑到了exec()代码时,位于exec()后面的代码就不会再被执行,除非我们使用quit、exit等退出语句来退出事件循环,退出后,程序才会继续执行位于exec()后面的代码。

2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码

线程停止函数的区别,从Qt源码来分析:

(1)QThread::quit()、QThread::exit()

//QThread::quit()声明 void quit(); //QThread::quit()定义 void QThread::quit() { exit(); } //QThread::exit()声明 void exit(int retcode = 0); //QThread::exit()定义 void QThread::exit(int returnCode) { Q_D(QThread); QMutexLocker locker(&d->mutex); d->exited = true; d->returnCode = returnCode; d->data->quitNow = true; for (int i = 0; i < d->data->eventLoops.size(); ++i) { QEventLoop *eventLoop = d->data->eventLoops.at(i); eventLoop->exit(returnCode); } }

由以上源码可知,QThread::quit() 和 QThread::exit(0) 的调用是等效的,都是告诉线程的事件循环,以返回码0(成功)退出。如果线程没有事件,则此函数不执行任何操作,也就是无效的。当线程拥有事件循环并且正处于 事件循环(QThread::exec()) 的状态时,调用 QThread::quit()或者QThread::exit() 线程就会马上停止,否则不会立刻停止线程,直到线程处于事件循环也就是正在执行 QThread::exec() 时,才会停止线程。

如果重复调用 QThread::quit()或者QThread::exit() 会有什么影响吗? 重复调用 QThread::quit()或者QThread::exit() 也不会有什么影响,因为只有拥有事件循环的线程,这两个函数才会生效停止线程的功能。

(2)QThread::terminate()

void QThread::terminate() { Q_D(QThread); QMutexLocker locker(&d->mutex); if (!d->running) return; if (!d->terminationEnabled) { d->terminatePending = true; return; } // Calling ExitThread() in setTerminationEnabled is all we can do on WinRT #ifndef Q_OS_WINRT TerminateThread(d->handle, 0); #endif QThreadPrivate::finish(this, false); //结束线程函数 }

在这个函数定义的最后一个语句,是调用了 QThreadPrivate::finish(this, false); 函数,其函数作用是直接退出线程,无论线程是否开启了事件循环都会生效,会马上终止一个线程,但这个函数存在非常不安定因素,不推荐使用。

如果重复调用 QThread::terminate() 会有什么影响吗? 没有影响。我们可以看到函数体里面的第三条语句,它首先会判断线程是否还在运行中,如果不是,会直接退出函数,就不会继续往下执行调用QThreadPrivate::finish(this, false); 函数了。

2.6 章节小结

相信看了以上的一些QThread源码,都大概知道了QThread类的本质以及QThread开启到结束的过程。这里我再简单总结下:

(1)QThread的本质:

QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西; QThread 所依附的线程,就是执行 QThread t 或 QThread * t=new QThread 所在的线程; QThread 管理的线程,就是 run 启动的线程,也就是次线程。

(2)在这里针对Qt4.4版本之后(包括Qt4.4版本)简单汇总一下线程启动到结束的过程:

QThread对象或者QThread派生类对象显式调用QThread类中的外部start()方法; QThread::start()方法再调用QThreadPrivate::start()方法; 在QThreadPrivate::start()方法内调用了QThread::run()虚函数,对使用者来说到了这里才是真正进入了一个新的线程里面。也就是说定义QThread对象或者QThread派生类对象的时候,还是在原来的线程里面,只有进入run函数才是进入了新的线程; 在QThreadPrivate::start()方法调用QThread::run()虚函数结束后,就会继续调用QThreadPrivate::finish()函数来结束线程,并发出线程结束的信号finished()。

(3)QThread::quit()、QThread::exit()、QThread::terminate():

对线程重复使用这三个停止线程的函数,没有任何影响; 尽量不要使用QThread::terminate()停止线程,此方式是强制退出线程,没有安全保障。 调用QThread::quit()和QThread::exit()一样。

(4)Qt各版本QThread类的变化:

Qt4.4版本之前QThread类是属于抽象类, Qt4.4版本之后(包括4.4版本)不是抽象类。 三、四种Qt多线程的实现方法

Qt的多线程实现方法主要有四种形式:子类化QThread、子类化QObject+moveToThread、继承QRunnable+QThreadPool、QtConcurrent::run()+QThreadPool。本文章会注重介绍前两种实现方法:子类化QThread、子类化QObject+moveToThread,也会简单介绍后两种的使用。 注:QtConcurrent、QRunnable以及QThreadPool的类,在Qt-4.4版本才开始有。

3.1 子类化QThread

子类化QThread来实现多线程, QThread只有run函数是在新线程里的,其他所有函数都在QThread生成的线程里。正确启动线程的方法是调用QThread::start()来启动,如果直接调用run成员函数,这个时候并不会有新的线程产生( 原因: 可以查看第一章,run函数是怎么被调用的)

3.1.1 步骤 子类化 QThread; 重写run,将耗时的事件放到此函数执行; 根据是否需要事件循环,若需要就在run函数中调用 QThread::exec() ,开启线程的事件循环。事件循环的作用可以跳到《2.4 QThread::run()源码》小节进行阅读; 为子类定义信号和槽,由于槽函数并不会在新开的线程运行,所以需要在构造函数中调用 moveToThread(this)。 注意:虽然调用moveToThread(this)可以改变对象的线程依附性关系,但是QThread的大多数成员方法是线程的控制接口,QThread类的设计本意是将线程的控制接口供给旧线程(创建QThread对象的线程)使用。所以不要使用moveToThread()将该接口移动到新创建的线程中,调用moveToThread(this)被视为不好的实现。

接下来会通过《使用线程来实现计时器,并实时在UI上显示》的实例来说明不使用事件循环和使用事件循环的情况。(此实例使用QTimer会更方便,此处为了说明QThread的使用,故使用线程来实现)

3.1.2 不使用事件循环实例

InheritQThread.hpp

class InheritQThread:public QThread {     Q_OBJECT public:     InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){              }     void StopThread(){         QMutexLocker lock(&m_lock);         m_flag = false;     } protected:     //线程执行函数     void run(){         qDebug()setText("Running");         }else{             ui->label->setText("Finished");         }     } private:     Ui::MainWindow *ui;     InheritQThread *WorkerTh; };

在使用多线程的时候,如果出现共享资源使用,需要注意资源抢夺的问题,例如上述InheritQThread类中m_flag变量就是一个多线程同时使用的资源,上面例子使用 QMutexLocker+QMutex 的方式对临界资源进行安全保护使用,其实际是使用了 RAII技术:(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。具体 QMutexLocker+QMutex 互斥锁的原理以及使用方法,在这里就不展开说了,这个知识点网上有很多非常好的文章。

效果:

(1)在不点【start】按键的时候,点击【check thread state】按钮检查线程状态,该线程是未开启的。

(2)按下【start】后效果如下,并查看终端消息打印信息:

只有调用了QThread::start()后,子线程才是真正的启动,并且只有在run()函数才处于子线程内。

(3)我们再试一下点击【stop】按钮,然后检查线程的状态:

点击【stop】按钮使 m_flag = false, 此时run函数也就可以跳出死循环,并且停止了线程的运作,之后我们就不能再次使用该线程了,也许有的人说,我再一次start不就好了吗?再一次start已经不是你刚才使用的线程了,这是start的是一个全新的线程。到此子类化 QThread ,不使用事件循环的线程使用就实现了,就这么简单。

3.1.3 使用事件循环实例

run函数中的 while 或者 for 循环执行完之后,如果还想让线程保持运作,后期继续使用,那应该怎么做? 可以启动子线程的事件循环,并且使用信号槽的方式继续使用子线程。注意:一定要使用信号槽的方式,否则函数依旧是在创建QThread对象的线程执行。

在run函数中添加QThread::exec()来启动事件循环。(注意: 在没退出事件循环时,QThread::exec()后面的语句都无法被执行,退出后程序会继续执行其后面的语句); 为QThread子类定义信号和槽; 在QThread子类构造函数中调用 moveToThread(this)(注意: 可以实现构造函数在子线程内执行,但此方法不推荐,更好的方法会在后面提到)。

接着上述的实例,在InheritQThread类构造函数中添加并且调用moveToThread(this);在run函数中添加exec();并定义槽函数:

/**************在InheritQThread构造函数添加moveToThread(this)**********/ InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){ moveToThread(this); } /**************在InheritQThread::run函数添加exec()***************/ void run(){ qDebug()


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有